Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net

...powered by www.netzwerkartist.de...

Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 9 Threads
  gp 9.1 Grundlegende Begriffe
  gp 9.2 Thread-Start
  gp 9.3 Thread-Zustände
  gp 9.4 Thread-Methoden
  gp 9.5 Race-Condition
  gp 9.6 Synchronisation und Deadlock
  gp 9.7 Vermeidungsstrategien zu Deadlocks
  gp 9.8 Guarded-Method: wait() und notify()
  gp 9.9 Weitere thread-sichere Maßnahmen
  gp 9.10 Thread-Mechanismen
  gp 9.11 Client-aktivierte asynchrone Methoden
  gp 9.12 Server-Aktivierung
  gp 9.13 Thread-Unterbrechung
  gp 9.14 Unbehandelte Ausnahmen in Threads
  gp 9.15 Zusammenfassung
  gp 9.16 Testfragen19

Kapitel 9 Threads

Die direkte Unterstützung von Threads in Java ist neu im
Vergleich zu C/C++, die hierfür keine direkte Sprachunterstützung vorsehen.
Der sinnvolle Einsatz von Threads macht Programme reaktiver, da mehrere Aktivitäten quasi gleichzeitig ausgeführt
werden können, und die Reaktionszeit gegenüber einem rein sequenziellen Ablauf zunimmt.
Die Darstellung des Themas »Threads« - beschränkt auf ein Kapitel - bedeutet die Vermittlung von Standardwissen zu grundlegenden Mechanismen und einige Regeln und Muster, vor allem zur Vermeidung von Problemen.

Threads:
nebenläufige leichtgewichtige Prozesse

Threads werden auch als nebenläufige leichtgewichtige Prozesse bezeichnet. Nebenläufig, weil sie quasi parallel nebeneinander her laufen, leichtgewichtig, weil sie sich mit anderen Threads dieselben Daten teilen.

Damit können Threads vom Betriebssystem schneller abwechselnd ausgeführt werden als Prozesse, d.h. parallel laufende Programme.

Jeder Thread ist ein einzelner sequenzieller Weg der Ausführung von Code. Dabei stehen die Threads an verschiedenen Stellen im gemeinsam genutzten Code und konkurrieren untereinander beim Zugriff auf Daten bzw. Objekte.

Diese Parallelität, der Einsatz von aktiven gegenüber passiven Objekten, ist eine Herausforderung, die zu neuen Regeln und Mustern führt.


Galileo Computing

9.1 Grundlegende Begriffe  downtop


Galileo Computing

9.1.1 Multi-Tasking, Multi-Threading  downtop

Von Betriebssystemen (OS) her kennt man zwei grundlegende Begriffe:

Multi-Tasking

gp  Unter Multi-Tasking versteht man die quasi-parallele Ausführung von Programmen unter einem OS (siehe Abb. 9.1).

Multi-Threading

gp  Unter Multi-Threading versteht man die quasi-parallele Ausführung von Code-Abschnitten auf gemeinsamen Daten und Objekten in nur einem Programm (siehe Abb. 9.2).

Bei Multi-Tasking kann man also mehrere (gleiche oder unterschiedliche) Programme separiert voneinander ausführen.

Threads arbeiten mit gemeinsamen Daten

Threads werden dagegen innerhalb eines Programms mit gemeinsamen Daten und Objekten ausgeführt, wobei dann Änderungen durch ein Thread Auswirkungen auf die anderen hat.

Prozess-
Ausführung


Abbildung
Abbildung 9.1   Multi-Tasking

Natürlich gibt es auch für die Prozess-Kommunikation einen vom Betriebssystem zur Verfügung gestellten Datenspeicher, der zum Datentransfer genutzt werden kann (siehe Abb. 9.1).


Abbildung
Abbildung 9.2   Multi-Threading

Lokale Variablen sind thread-eigen

Umgekehrt sind lokale Variablen von Methoden thread-eigen, wobei aber bei lokalen Referenzen zwischen der thread-eigenen Referenz-Variablen und dem referenzierten Objekt unterschieden werden muss.


Galileo Computing

9.1.2 Scheduling, Priorität und Preemption  downtop

Prozesse und Threads werden von einem Scheduler (Planer) im Betriebssystem verwaltet, d.h., alle aktiven Threads werden in eine Warteschlange eingeordnet und je nach Priorität für kurze Zeitintervalle ausgeführt.

Scheduling:
Ausführungsplan

gp  Mit Scheduling bezeichnet man den Ausführungsplan zum Umschalten der aktiven Threads. Scheduling beruht auf Preemption und/oder Time-Slicing.

Priorität - Preemption

gp  Jeder Thread hat eine Priorität. Bei Preemption (nach dem engl. Verb preempt, dt. zuvorkommen) werden immer die Threads mit der höchsten Priorität ausgeführt, die anderen müssen warten.

Time-Slicing

gp  Beim Time-Slicing kommt nach einem gewissen Zeitintervall - auch Zeitscheibe genannt - immer ein anderer Task/Thread zur Ausführung, ohne dass der laufende darauf Einfluss hat.

Die Art des Schedulings kann nicht in Java spezifiziert werden. Java-Prioritäten werden auf Betriebssystems-Prioritäten abgebildet, wobei diese Abbildung nicht 1:1 und keineswegs eindeutig ist. Nach welchen Heuristiken der Scheduler Threads dann noch Zeitscheiben zuordnet, ist nicht vorhersehbar.

Nicht deterministisches Scheduling

Der Ausführungsplan muss also als nicht deterministisch angesehen werden. Wann und wie lange welcher Thread zur Ausführung kommt, hängt vom Scheduler und dem jeweiligen Zustand des Systems ab.


Abbildung
Abbildung 9.3   Ausführung der Methode obj.service() in zwei Threads

Eine Methode, z.B. service(), kann dabei durchaus von zwei oder mehreren Threads für dasselbe Objekt obj gleichzeitig ausgeführt werden (siehe Abb. 9.3).

Dies macht Laufzeittests auf Fehlerfreiheit mühsam, da sich aufgrund der Testläufe nicht schlüssig ermitteln lässt, wie die nächste Ausführung beim Kunden aussieht.


Galileo Computing

9.1.3 Synchronisation  downtop

Parallel laufende Threads sind prinzipiell in ihrem Kontrollfluss bzw. in ihren Aktivitäten unabhängig. Dies bedeutet, dass sich alle Threads erst einmal asynchron verhalten, zu jedem Zeitpunkt jede Instanz- oder Klassen-Methode ausführen können.

Synchrones vs. asynchrones
Verhalten

gp  Jede Art des Wartens aufeinander nennt man Synchronisation, das Gegenteil ist asynchrones Verhalten.

Wird eine Methode aufgerufen, ohne auf ihr Operationsende zu warten, ist dies ein asynchroner Aufruf, ansonsten ein synchroner. Wartet ein Thread auf die Beendigung eines anderen, nennt man sie sychronisiert.

Locking: Sperren von Objekten bzw. Klassen

Java synchronisiert grundsätzlich mit Hilfe von Locks (Sperren) auf Objekte oder auf Klassen (einem Lock auf Class-Objekte). Jedes Objekt bzw. jede Klasse (Class-Objekt) hat genau ein Lock.

synchronized

Wird irgendeine Klassen- oder Instanz-Methode mit dem Schlüsselwort synchronized markiert, so muss sich ein Thread für die Ausführung dieser Methoden erst einmal das zugehörige Objekt- bzw. Klassen-Lock holen.


Abbildung
Abbildung 9.4   Synchronisierte Ausführung von obj.service()

Besitz des Locks: exklusive Ausführungsrechte

Während ein Thread (Thread 1 in Abb. 9.4) ein Lock besitzt, kann kein anderer dieses erwerben und hat somit keinerlei Ausführungsrechte für alle synchronisierten Methoden dieses Objekts bzw. dieser Klasse.

Der Thread, der das Lock für eine synchronisierte Methode erworben hat, kann nicht nur die eine, sondern alle synchronisierten Methoden des Objekts bzw. der Klasse exklusiv ausführen.

Nachdem ein Thread die letzte synchronisierte Methode eines Objekts bzw. einer Klasse beendet hat, wird das entsprechende Lock wieder freigegeben und kann von einem anderen Thread erworben werden (Thread 2 in Abb. 9.4).

Deadlock

Deadlock: Gegenseitige Blockade von Threads

Mit Einführung der Synchronisation besteht die Gefahr, dass zwei Threads gegenseitig aufeinander warten oder - schlimmer - alle aufeinander warten, d.h., das gesamte System »friert ein«.

Diese Situation ist aus dem Straßenverkehr bekannt, wo an einer Kreuzung vier Fahrzeuge aus den verschiedenen Richtungen gleichzeitig ankommen und aufgrund der Rechts-vor-Links-Regel alle aufeinander warten müssen. Dieser Deadlock wird dann durch einen mutigen Fahrer bzw. einen Unfall beseitigt.


Galileo Computing

9.2 Thread-Start  downtop

Zwei Alternativen zum Starten eines Threads

Startpunkt eines Threads ist immer ein Objekt der Klasse Thread, wobei es allerdings zwei Alternativen gibt (Abb. 9.5).


Abbildung
Abbildung 9.5   Zwei Alternativen zum Starten eines Threads

zu A1: Dies ist eher die Ausnahme. Will man wirklich nur einen Thread erschaffen, dann ist diese Alternative ok und auch einfacher (Abb. 9.6).

zu A2: Dies ist der Normalfall. Man will Objekte konkreter Klassen wie z.B. einen Druck-Manager, einen Video-Player, eine Kalkulation oder Transaktion erschaffen, welche ihren Service asynchron ausführen sollen.

Service als Thread oder asynchroner Dienst

Dies bedeutet aber, dass man eine von der Thread-Klasse unabhängige Klasse bzw. Klassen-Hierarchie entwerfen muss, mit der zusätzlichen Eigenschaft, asynchron ausgeführt werden zu können (Abb. 9.6).


Abbildung
Abbildung 9.6   Service-Klasse als Sub-Thread vs.»mit asynchronem Dienst«
Galileo Computing

9.2.1 Methoden run() und start()  downtop

main() und run():
Start- und Endpunkt der Ausführung

Die Methode run() ist äquivalent zur Methode main() einer Applikation. Die Methode main() bildet den Start- bzw. Endpunkt des Hauptthread, run() den Start- und Endpunkt eines zusätzlichen Threads. Thread wie Applikation sind mit dem Ende von run() bzw. main() beendet.

Ist run() beendet, sagt man auch, dass der Thread den Endzustand tot (dead) erreicht hat, kurz tot ist.

Icon

Es sind folgende vier Regeln zur Ausführung wichtig:

Thread-
Ausführung

1. Ein Thread muss immer mittels start() gestartet werden, der direkte Aufruf von run() führt zwar die Methode aus, aber nicht als neuen, eigenständigen Thread.
2. Ein Thread, der beendet (tot) ist, kann nicht wieder neu gestartet werden, weder mittels start() noch mittels run().

3. Um ein Thread erneut zu starten, muss eine neue Thread-Instanz erschaffen und wieder mittels start() gestartet werden.
4. Das thread-fähige Objekt existiert unabhängig von dem (System-) Thread, den es auslöst (siehe Abb. 9.6). Die Methoden des Objekts können unabhängig davon, in welchem Zustand der Thread ist, immer von allen anderen Threads ausgeführt werden.1

Thread-Start-Muster

Icon

Nachfolgend zwei einfache Muster, um einen Thread zu erzeugen:

Muster für einen Thread-Start

// 1. Muster
class SubThread extends Thread { public void run() { ... } // siehe 1. Regel }
// 2. Muster
class ClassWithThread implements Runnable { public void run() { ... } // siehe 1. Regel }
// Testklasse zur Anlage und Start der Threads
public class TestThread { public static void main(String[] args) { Thread st1= new SubThread(); Thread st2 = new Thread(new ClassWithThread());
    st1.start();     // Methode start() ruft run() 
auf
    st2.start();     // run() läuft asynchron zu main()
  }
}

Dieses Testbeispiel zeigt noch ein interessantes Detail:

Ende einer
Java-App

gp  Eine Java-App läuft so lange, bis der letzte Nicht-Dämon-Thread beendet ist.

Im Testbeispiel ist zwar main(), d.h. der Hauptthread, sofort nach Aufruf der beiden Methoden start() beendet, aber die Java-App ist erst beendet, wenn die beiden anderen Threads beendet sind.

Würde man in main() die Methode st1.run() anstatt st1.start() ausführen, wäre dies eine synchrone Ausführung im Hauptthread (siehe 4. Regel).

Bei synchroner Ausführung muss zuerst st1.run() beendet werden, bevor die nächste Anweisung st2.start() ausgeführt wird.


Galileo Computing

9.3 Thread-Zustände  downtop

Zustandswechsel einer Thread

Um die Methoden, die auf Threads operieren, zu verstehen und sinnvoll zu benutzen, muss man die möglichen Zustände kennen, in denen sich ein Thread befinden kann (Abb. 9.7).


Abbildung
Abbildung 9.7   Diagramm der Thread-Zustände

Zustand: aktiv bzw. tot


Galileo Computing

9.3.1 Zustand: aktiv bzw. tot  downtop

Nach dem Start des Threads wird der Thread aktiv und wechselt aufgrund des Scheduling zwischen lauffähig und code-ausführend (»läuft«).

Ist run() beendet oder hat man die unsichere deprecated Methode stop() aufgerufen, so ist der Thread beendet, kurz tot.

Suspendierende Zustände

Daneben gibt es noch vier Zustände, in die ein Thread absichtlich oder unbeabsichtigt geraten kann, und die man auch generell mit dem Begriff suspendierend bezeichnen kann. Es ist aber wichtig, sie aufgrund ihres Typs zu unterscheiden.


Galileo Computing

9.3.2 Zustand: schlafend  downtop

Zustand:
schlafend

Der Zustand »schlafend« wird nur durch die statische Thread-Methode sleep(long ms) ausgelöst und lässt den ausführenden Thread für die angegebene Zeit in Millisekunden ruhen. Danach geht er wieder in den aktiven Zustand über.


Galileo Computing

9.3.3 Zustand: blockiert vs. nicht blockiert bei I/O  downtop

Zustand: blockiert vs. nicht blockiert bei Input/Output

Ist ein Thread mit Daten-Ein-/Ausgabe (I/O) beschäftigt, muss er eventuell aufgrund nicht bereiter I/O-Geräte warten. Dies ist eigentlich sogar der Normalfall, da in der Regel Peripherie-Geräte wesentlich langsamer in der Ausführung sind als die CPU.

Ein Thread, der auf I/O wartet, wird von der JVM suspendiert bzw. blockiert, damit andere aktive Threads ausgeführt werden können. Der Thread wird erst wieder aktiv, wenn die Daten bereitstehen oder geschrieben wurden. Diese Art der Ein-/Ausgabe ist also synchron.

»Friert« ein I/O-Gerät ein, bedeutet dies, dass der zugehörige Thread nicht mehr läuft, was auch eine Art von Tod darstellt.

nonblocking I/O

Das Gegenteil ist »nicht blockierend bei I/O« (nonblocking I/O). Hier wartet der Thread nicht. Liegen Daten von der Eingabe bereit, liest er sie oder er kehrt sofort wieder zurück. Kann der Thread bei der Ausgabe Daten nicht schreiben, kehrt er ebenfalls sofort zurück.

Keine asynchrone I/O

gp  Java unterstützt - mit wenigen Ausnahmen - nur synchrone I/O, d.h., es gibt keine direkte Unterstützung für eine nicht blockierende I/O.

Galileo Computing

9.3.4 Zustand: Warten auf Lock  downtop

Zustand: Warten auf Lock

Der Mechanismus der Synchronisation wurde bereits in 9.1.3 beschrieben. An dem Zustands-Diagramm in 9.7 erkennt man schon die Gefahr des Deadlocks. Wartet ein Thread auf den Eintritt in einen synchronisierten Code-Bereich und bekommt das Lock nicht, ist er so gut wie tot.


Galileo Computing

9.3.5 Monitor  downtop

Bei Threads, die auf denselben Objekten operieren, gibt es zwei Möglichkeiten der Kontrolle.

Entweder prüfen die Threads, ob der Zustand der Objekte passend ist, oder umgekehrt, die Objekte kontrollieren aufgrund ihrer Zustände die Threads. Bei Java hat man sich für die zweite Möglichkeit entschieden.

Monitor: Die Objekte kontrollieren die Threads

gp  Ein Objekt, das die auf ihm operierenden Threads suspendieren und wieder aktivieren kann, heisst Monitor.
gp  Jedes Objekt, das synchronisierten Code enthält, ist ein Monitor.

Galileo Computing

9.3.6 Methoden wait(), notify() bzw. notifyAll()  downtop

Synchronisation mittels wait() und notify()

Zur Kontrolle der Threads hat die Klasse Object und somit jedes Objekt die Methoden wait() und notify() bzw. notifyAll().

Innerhalb einer synchronisierten Methode versetzt wait() einen Thread in den Zustand »wartend«.

Ist ein passender Zustand erreicht, kann das Objekt eine oder alle wartenden Threads mittels notify() bzw. notifyAll() in der gleichen oder einer anderen synchronisierten Methode wieder aktivieren. Dieser Aufruf kann natürlich nur aus einem anderen aktiven Thread heraus erfolgen (siehe Abb. 9.8).


Abbildung
Abbildung 9.8   Objekt als Monitor mit Methoden wait() und notify()
Galileo Computing

9.3.7 Zustand: wartend  downtop

Zustand: wartend

Jede Instanz hat einen eigenen Zustand »wartend«, in den alle Threads, die wait() auf diese Instanz ausführen, überführt werden (siehe Abb. 9.8, Thread 1).

Icon
notify(): Verlassen des Wartezustands

Kurz zu den Regeln, verbunden mit notify():

gp  Wird in derselben Instanz ein notify() von einem noch aktiven Thread ausgeführt und mehr als ein Thread wartet, wird ein zufälliger Thread in den aktiven Zustand überführt.

Kein FIFO-Prinzip

gp  Es gibt bei notify() keine Reihenfolge à la FIFO (First-In First-Out ).
gp  Die Methode notifyAll() aktiviert alle zur Instanz wartenden Threads.

Wer zuerst wartet, wird also bei notify() bzw. notifyAll() nicht unbedingt zuerst wieder aktiviert. Die Auswahl eines Threads ist willkürlich (in Abb. 9.8 gibt es allerdings nur einen wartenden Thread 1).

Für statische Methoden läuft der oben beschriebene Mechanismus analog, nur eben auf Klassen-Ebene, d.h., der Monitor ist das Objekt Class.


Galileo Computing

9.3.8 Unterbrechen der Zustände  downtop

Wie in Abb. 9.7 zu sehen, können die suspendierenden Zustände »schlafend« oder »wartend« unterbrochen werden. Dazu muss zu diesem Thread die Instanz-Methode interrupt() von einem anderen laufenden Thread aufgerufen werden.

interrupt(): Unterbrechung der Zustände wartend und schlafend

Die Methode interrupt() bewirkt bei einem

gp  suspendierten Thread einen Übergang nach »aktiv« und die Auslösung einer InterruptedException, die - da keine Runtime-Exception - abgefangen werden muss.

Interrupt-Flag

gp  aktiven Thread, dass ein Interrupt-Flag gesetzt wird, das entweder während der Ausführung abgefragt werden kann oder beim nächsten suspendierenden Zustand dann die Ausnahme auslösen sollte (Näheres siehe 9.13).

InterruptedIOException, im Prinzip ja, aber ...

Korrekterweise müsste in Abb. 9.7 auch beim Verlassen des Zustands »blockiert« ein »unterbrochen« eingetragen werden, denn auf eine Unterbrechung sollten I/O-Methoden mit einer InterruptedIOException reagieren.

Das ist aber bei den meisten IO-Methoden nicht implementiert, selbst System.in von Sun reagiert darauf nicht. Deshalb wurde dieser Guard weggelassen. Hier helfen nur Abbrüche durch Time-outs.


Galileo Computing

9.4 Thread-Methoden  downtop

Die Klasse Thread enthält als zentraler Repräsentant weitere wichtige Methoden, die kurz vorgestellt werden sollen.


Galileo Computing

9.4.1 Konstruktoren  downtop

Es gibt drei bzw. vier Konstuktoren für die beiden Alternativen der Thread-Erzeugung (siehe 9.2). Für die zweite Alternative muss bei der Anlage der Thread-Instanz ein target-Objekt übergeben werden, das run() enthält. Jedem Thread kann optional ein Name gegeben und eine Thread-Gruppe zugeordnet werden.

Thread-Gruppen

gp  Thread-Gruppen sind dazu da, Threads nach ihrer Funktionalität zu gruppieren, um dann alle Threads einer Gruppe gleichzeitig manipulieren zu können.

Nachfolgend die sieben Konstruktoren (target teilweise optional):

Thread-
Konstruktoren

 public Thread( [Runnable target] );
 public Thread( [Runnable target,] String name);
 public Thread(ThreadGroup group, Runnable target);
 public Thread(ThreadGroup group, [Runnable target,] String name);

Galileo Computing

9.4.2 Statische Methoden  downtop

Statische Methoden der Klasse Thread

Es folgen einige wichtige statische Methoden:

gp  static int activeCount(): Ermittelt die Anzahl der zur Zeit aktiven Threads der aktuellen Thread-Gruppe.
gp  static Thread currentThread(): Ermittelt den zur Zeit laufenden (code-ausführenden) Thread.
gp  static boolean interrupted(): Ein Thread kann einen anderen unterbrechen (siehe 9.3.8). Bei einem aktiven Thread wird das »unterbrochen«-Flag mit interrupted() getestet und anschließend wieder auf false gesetzt (siehe Alternative isInterrupted() in 9.4.4).
gp  static void sleep(long ms) throws InterruptedException: Der laufende Thread wird für die angegebene Zeit in Millisekunden in den Zustand »schlafend« versetzt.
gp  static void yield(): Überlässt einem anderen lauffähigen Thread die Möglichkeit der Ausführung (ist abhängig vom Scheduler!).

Galileo Computing

9.4.3 Prioritäten  downtop

Prioritäten: MIN_PRIORITY .. MAX_PRIORITY

Jeder Thread hat eine Priorität (siehe 9.1.2). Java kennt Prioritäten zwischen static final int MIN_PRIORITY (=1) und MAX_PRIORITY (=10).

Die vorgegebene Default-Priorität ist NORM_PRIORITY (=5), womit z.B. der Hauptthread, gestartet durch main(), beginnt.

gp  Java-Prioritäten können nur im Idealfall 1:1 auf Betriebssystems-Prioritäten abgebildet werden.

Scheduler und
Prioritäten

In welchem Maße der Scheduler Threads mit höherer Priorität bevorzugt, ist nicht festgelegt. Es liegt zwischen den Extrema »nur Threads mit der höchsten Priorität sind laufberechtigt« und »alle Threads, egal welcher Priorität, sind gleich laufberechtigt«.

Die Priorität eines Threads kann zur Laufzeit gelesen und gesetzt werden (siehe 9.4.4).

Icon

Für die Prioriät beim Start gilt folgende Regel:

Default-Priorität eines Threads

gp  Ein neuer Thread übernimmt bei der Anlage die Prioriät des Threads, aus dem er erschaffen wurde.

Galileo Computing

9.4.4 Instanz-Methoden  downtop

Instanz-Methoden der Klasse Thread

Von den insgesamt 20 nicht deprecated Instanz-Methoden werden hier nur die wichtigsten, noch nicht besprochenen vorgestellt.

gp  public final int getPriority(),
public final void
setPriority(int newPriority): Liest oder setzt die Priorität dieses Threads.
gp  public final ThreadGroup getThreadGroup(): Liest die Thread-Gruppe, zu der dieser Thread gehört.
gp  public void interrupt(): Löst für diesen Thread eine Unterbrechung aus (Details siehe 9.3.8).
gp  public boolean isInterrupted(): Testet für diesen Thread, ob eine Unterbrechung vorliegt, ohne das »unterbrochen«-Flag zu verändern (siehe Alternative interrupted()).
gp  public final native boolean isAlive(): Liefert true, wenn dieser Thread gestartet wurde, aber noch nicht tot ist, ansonsten false.
gp  public final void join( [long ms] ) throws InterruptedException: Blockiert den Thread, der join() ausführt, so lange bis der Thread, verbunden mit der Instanz, stirbt bzw. tot ist. Bei einer Zeitangabe ist der Thread maximal ms Millisekunden blockiert (siehe auch Abb. 9.7).

Dämon-Threads:
ohne eigene Existenzberechtigung

gp  public final void setDaemon(boolean on): Setzt den Thread als Dämon-Thread (im Sinne von Hintergrunds-/Dienst-Thread). Diese Anweisung hat unbedingt vor dem Thread-Start zu erfolgen.
Dämon-Threads zählen bei der Entscheidung nicht mit, ob das Programm von der JVM weiter ausgeführt werden muss. Gibt es nur noch Dämon-Threads, ist die Ausführung beendet.
Galileo Computing

9.4.5 Diverse Methoden im Beispiel  downtop

Nach dem Überblick über Konzepte und Instrumente soll ein erstes kleines Beispiel den Einsatz und die Wirkung einiger der genannten Methoden demonstrieren.

Zuerst werden zwei Threads nach den beiden in 9.2 beschriebenen Alternativen angelegt und dann diverse statische und nicht statische Methoden der Thread-Klasse getestet:

// Hilfs-Klasse 
class TestRun { 
 // der aktuelle Thread maximal n Mal für
// sleepTime Millisec. schlafen lassen
static void run(int sleepTime, int n ) { try { while (n-->0) { System.out.println(Thread.currentThread().getName()); Thread.sleep(sleepTime); } } catch (InterruptedException e) { System.out.println("Interrupted: "+Thread.currentThread()); } } }

Subklasse von Thread: Overriding run()

class SubThread extends Thread {
   public void run() { 
     TestRun.run(1000,3); // dreimal 1 sec schlafen
  } 
}

Implementation von Runnable

class ClassWithThread implements Runnable {
  public void run() {
   TestRun.run(1000,3); // dreimal 1 sec schlafen
  }
}

Test diverser Thread-Methoden

public class Test {
  public static void main(String[] args) {
    Thread st0= new SubThread();
    Thread st1= new Thread(new ClassWithThread());
    // st1.setDaemon(true);                        
             ¨
    System.out.println(Thread.currentThread().getThreadGroup());
    System.out.println(Thread.currentThread().getPriority());
    st0.start(); st1.start(); 
    st0.interrupt();
  }
}
Tabelle 9.1   Mögliche Konsol-Ausgabe zur Applikation »Test«
 java.lang.ThreadGroup[name=main,maxpri=10]
 5
 Thread-1
 Thread-0
 Interrupted: Thread[Thread-0,5,main] 
 Thread-1
 Thread-1

Default-Thread-Gruppe

Erklärung: Das erste println() gibt mit Hilfe von toString() die Standard-Thread-Gruppe aus, zu der der Hauptthread gehört. Threads ohne Namen werden automatisch mit Null beginnend numeriert. Danach folgt die Prioritäts-Angabe des Hauptthreads.

Obwohl Thread-0 vor Thread-1 gestartet wurde, entscheidet allein der Scheduler über die CPU-Zuteilung, d.h., wer sich auf der Konsole zuerst meldet, ist Sache des Schedulers.

Thread-0 wird im Schlaf-Zustand unterbrochen, was eine InterruptedException auslöst (siehe Konsol-Meldung). Thread-1 durchläuft dagegen dreimal die Schleife, was zu den letzten beiden Ausgaben führt.

Nur noch
Dämon-Threads: Programmende

zu ¨: Wird die auskommentierte Anweisung ausgeführt, ist Thread-1 ein Dämon. Das Programm ist somit beendet, wenn Hauptthread sowie Thread-0 terminieren, d.h., Thread-1 wird sich nur einmal melden.


Galileo Computing

9.5 Race-Condition  downtop

Wenn Threads parallel auf dieselben Objekte zugreifen können, wird die Frage interessant, in welcher Reihenfolge Lese- und Schreib-Operationen auf den Objekten ablaufen.


Galileo Computing

9.5.1 Atomare vs. nicht atomare Operationen  downtop

Atomare Operation: nicht unterbrechbar

Unter einer atomaren Operation versteht man eine Operation, die entweder als Ganzes oder überhaupt nicht durchgeführt wird. Beginnt die Operation, kann sie nicht mehr unterbrochen werden, sie ist unteilbar. Das Gegenteil ist nicht atomar.

Betrachten wir ein einfaches Objekt Int:

Primitive Typen == atomare Operationen ?

class Int {
  private int i;
  boolean test(int j) { 
    i= j; 
    return i==j;
} }

In einer single-threaded Applikation ist das Ergebnis der Methode test() trivial, nämlich true.

Bei Multi-Threading lautet die Frage, ob test() atomar ist. Die Methode ist dann atomar, wenn Setzen auf j und Vergleich mit j nicht unterbrechbar sind, also ganz oder gar nicht ablaufen.

Nachfolgend ein einfacher Test von Int mit Konsol-Ausgabe:

Testprogramm
zu atomaren Operationen

class AtomicTest implements Runnable 
{
  Int iobj;
  AtomicTest(Int iobj) { 
    this.iobj= iobj; 
  }
  public void run() {
    int i= 0;
    // maximal 100000 mal auf true testen
    while (iobj.test(i) && ++i <100000);
    System.out.println(Thread.currentThread().getName()+": "+i);
  }
}

In der Klasse Test wird die Methode test() von genau einer Int-Instanz parallel von vier Threads ausgeführt:

Ein Objekt, vier schreibende Threads

public class Test {
  public static void main(String[] args) {
    Int iobj= new Int();                  // nur ein Objekt
    Thread[] st= new Thread[4];

Lesen und Schreiben des
primitiven Typs int ist nicht atomar

    for (int i=0; i<st.length;i++) {
      st[i]= new Thread(new AtomicTest(iobj)); 
   // st[i].setPriority(Thread.MAX_PRIORITY-2*i);             ¨
      st[i].start();
    }                                            
  }
}
Tabelle 9.2   Drei von vielen möglichen Konsol-Ausgaben der Klasse Test
 Thread-2: 100000
 Thread-0: 100000
 Thread-1: 100000
 Thread-3: 100000
 Thread-0: 15
 Thread-1: 100000
 Thread-2: 100000
 Thread-3: 100000
 Thread-1: 0
 Thread-3: 0
 Thread-0: 7
 Thread-2: 100000

Erklärung: Die Methode test() in der Klasse Int ist nicht atomar, vor dem Vergleich von i mit j kann ein anderer Thread bereits den Wert von i geändert haben, was allerdings recht selten vorkommt.

Prioritäten

Mit unterschiedlichen Prioritäten kann man versuchen, Einfluss auf den CPU-Zuteilungs-Algorithmus des Schedulers zu nehmen.

Wird die auskommentierte Anweisung ¨ in Test ausgeführt, so erhalten die Threads abnehmende Prioritäten.

Bei preemptive Scheduling sollte sich dann Thread-0 zuerst melden, danach Thread-1, Thread-2 und Thread-3. Dies ist allerdings wieder vom Scheduler abhängig.


Galileo Computing

9.5.2 Reentrant, Race-Condition und thread-sicher  downtop

Im Zusammenhang mit Thread-Problemen sind folgende Begriffe wichtig:

Reentrant Code

gp  Ein Code-Abschnitt (Methode oder Block) heißt reentrant, wenn er von mehreren Threads gleichzeitig ausgeführt wird.

Im letzten Beispiel war die Methode test() reentrant. Ein Thread führt i=j; aus, während ein anderer den Vergleich i==j; durchführt.

Race-Condition: Thread-Wettrennen

gp  Unter Race-Condition versteht man Fehler bzw. Probleme, die durch reentrant Code entstehen, d.h., Ergebnisse werden von der zufälligen Reihenfolge parallel ablaufender Operationen abhängig.

Icon

In Verbindung mit Reentrance gilt folgende Regel:

Atomare
Operationen

gp  Nur Lese- oder Schreib-Operationen auf Variablen bis vier Byte Länge sind atomar. Alle anderen Operationen sind nicht atomar.

Icon
Vermeidlich atomare
Operationen

Somit sind die meisten einzelnen Anweisungen nicht atomar und können ergo zu Race-Conditions führen. Dies ist sicherlich nicht sehr glücklich. Man betrachte die beiden »unverdächtigen« Anweisungen zu einer int i bzw. long l:

     i++;      // muss gelesen und geschrieben werden
     return l; // 8 Byte benötigen zwei Lese-Operationen

Dies sind zwei überraschende Beispiele für nicht atomare Anweisungen.

Thread-sicher:
»no problems« bei Multi-Threading

gp  Code bzw. Methoden heißen thread-sicher (thread-safe), wenn es zu keinen Problemen in einer Multi-Threading-Umgebung kommt.

Können Race-Conditions auftreten, ist der Code nicht thread-sicher. Leider ist dies nicht hinreichend, will sagen, nicht das einzige Problem.

Thread-Sicherheit erfordert in einer komplexen Ablaufumgebung umfangreiche Maßnahmen, von denen im Weiteren nur wenige besprochen werden können.


Galileo Computing

9.6 Synchronisation und Deadlock  downtop

Guarded
C
oncurrency: Single-Thread-Ausführung durch Synchronisation

Mit Hilfe des Schlüsselworts synchronized werden Methoden oder Blocks vor Reentrance geschützt (guarded):

gp  Der mit synchronized geschützte Code kann nicht gleichzeitig von mehreren Threads ausgeführt werden.

Logisch atomare Operation

Werden in einem synchronisierten Code-Block keine suspendierenden Anweisungen ausgeführt und gibt es nur Operationen auf primitiven Datentypen, ist der Code-Block sogar logisch atomar, d.h., zuerst müssen alle Anweisungen von einem Thread durchlaufen werden, bevor ein anderer denselben Code-Block ausführt.

Die Methode test() der Klasse Int in 9.5.1 kann also ganz einfach atomar und in diesem Fall dann auch thread-sicher gemacht werden:

Synchronisierte primitive
Operationen: thread-sicher und logisch atomar

class Int {
  private int i;
  synchronized boolean test(int j) { // guarded concurrency
    i=j; 
    return i==j;
  }
}

Bevor einer der vier Threads die Methode test() in dem Beispiel in 9.5.1 ausführen darf, muss er vom Monitor - der Instanz iobj der Klasse Int - das Lock erhalten. Hat ein Thread das Lock, gehen die anderen bei Aufruf von test() in den Zustand »warten auf Lock« über (siehe Abb. 9.7).


Galileo Computing

9.6.1 Methoden- und Block-Synchronisation  downtop

Die Synchronisierung kann auf

Synchronisation einer Methode

gp  Methoden-Ebene erfolgen:
synchronized [Modifiers] ResultType method (pList) ...
Dann muss der ausführende Thread bei einer Instanz-Methode zuerst das Lock des zugehörigen Objekts this, bei einer statischen Methode das Lock des Class-Objekts erwerben.

Synchronisation eines Blocks

gp  Block-Ebene erfolgen:
synchronized(ReferenzExpression) { synchronizedBlock }
Dann muss der ausführende Thread zuerst das Lock des Objekts erwerben, auf das ReferenzExpression zeigt.

Icon

Es gelten folgende drei Regeln:

Beschränkungen für synchronized

1. Konstruktoren können nicht synchronized werden.
2. In Interfaces ist synchronized nicht erlaubt.
3. Methoden können synchronized oder nicht überschrieben werden.

Thread-sichere Konstruktoren

Konstruktoren:
thread-sicher?

Konstruktoren brauchen nur synchronisiert werden, sollten sie während der Initialisierung eine Referenz des unfertigen Objekts this an andere Objekte herausreichen. In diesem Fall muss dann eben Block-Sychronisation verwendet werden.

Alternative Synchronisations-Mechanismen

Block-Synchronisation ist feiner

gp  Die Sychronisation auf Block-Ebene ist feiner und kann die der Methoden emulieren (siehe nachfolgendes Beispiel).

Beispiel

Icon

In der Klasse Sync werden verschiedene äquivalente Synchronisations-Mechanismen demonstriert.

Synchronisations-Muster

Die statischen Methoden sf1(), sfV1() und sfV2() sowie die Instanz-Methoden f1() und f2() sind äquivalent.

class Sync { 

Synchronisation von statischen Methoden

  // Statisch: Methoden- vs. Block-Synchronisation
  public synchronized static void sf1() 
{  
    //..  
  }
  public static void sfV1() {
    try  {
      // Einsatz von Reflexion
synchronized(Class.forName("Sync")) { //.. } } catch (ClassNotFoundException e) { } } public static void sfV2() { synchronized(new Sync().getClass()) { //.. } }

Synchronisation von Instanz-Methoden

  // Instanz: Methoden- vs. Block-Synchronisation
  public synchronized void f1() {  

    //..
  }
  public void f2() {
    synchronized(this) {  
      //..
    }
  }
}

Vorteile der Methoden-Synchronisation

Vorteile der Methoden-
Synchronisation

gp  Methoden-Synchronisation ist sofort im Methoden-Kopf zu erkennen, Block-Synchronisation dagegen ohne Zugriff auf den Code nicht.

Sofern als Lock andere Objekte als die eigene Instanz oder Klasse verwendet werden, sind die Auswirkungen der Synchronisation schwer abzuschätzen. Wird dies vom Anwender nicht durchschaut, kann es schnell zu Deadlocks kommen (siehe 9.6.3).


Galileo Computing

9.6.2 Voll- bzw. teilsynchronisierte Objekte  downtop

Ein Thread, der den Lock besitzt, kann als einziger beliebige synchronisierte Methoden bzw. Blöcke desselben Objekts ausführen.

Erst wenn ein Thread die letzte synchronisierte Methode des Objekts verlassen hat, kann ein anderer Thread den Lock erlangen.

Voll- vs. Teil-
Synchronisation

Sind - bis auf die Konstruktoren - alle Methoden eines Objekts synchronisiert und alle Felder private erklärt, so heißt eine Objekt vollsynchronisiert, ansonsten teilsynchronisiert.

Icon

Ist ein Objekt

Thread-Eintritt in Objekten

gp  vollsynchronisiert, so kann nur ein Thread zu einem Zeitpunkt auf dem Objekt operieren.
gp  teilsynchronisiert, so können neben dem Thread, der das Lock besitzt, beliebig viele andere Threads unsynchronisierte Methoden parallel im Objekt ausführen.

Galileo Computing

9.6.3 Deadlock durch Synchronisation  downtop

Deadlock-
Situation

Bei einem Deadlock sind zumindest zwei Threads im Zustand »warten auf Lock«, das jeweils einen anderen Thread im selben Zustand hält. Diese Threads blockieren sich gegenseitig und können nicht mehr aktiv werden.

Gibt es keinen aktiven Thread mehr, friert eine Java-App ein.